﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

using MLP.NeuralNet;
using MLP.NeuralNet.Learning;

namespace NeuroProzessorSvc
{
    public class _cls_NeuroProzessor: MarshalByRefObject
    {
        private Network _network;
        private BackPropagationLearning _teacher;

        public RUNFLAG _LEARNSTATE = RUNFLAG.NONE;
        public float LAST_ERROR;

        /// <summary>
        /// Ausführungszustände des Lern-Threads
        /// </summary>
        public enum RUNFLAG
        {
            NONE = 0,
            RUNNING = 1,
            FORCESTOP = 99
        }

        private TcpClientChannel cli_RM_Neuro_Visual;
        private IVisual.I_visual cli_RM_frm_visual;

        /// <summary>
        /// Initialisiert das Remoting der Visualisierung (optional)
        /// </summary>
        /// <param name="IP">IP des Visu-Servers</param>
        /// <param name="Port">Port der Visu-Servers</param>
        private void INIT_Remote_Visual(string IP, int Port)
        {
            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.METHOD_INVOKE, "start");
            if (IP == "" || Port <= 0 || this.cli_RM_Neuro_Visual != null || IP == "0.0.0.0")
                return;

            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Remotevisualisierung wird initialisert.");
            cli_RM_Neuro_Visual = new TcpClientChannel("cli_RM_Neuro_Visual", null);
            ChannelServices.RegisterChannel(cli_RM_Neuro_Visual, false);
            cli_RM_frm_visual = (IVisual.I_visual)Activator.GetObject
            (
                typeof(IVisual.I_visual),
                "tcp://" + IP + ":" + Port + "/_RM_Neuro_Visual"
            );
            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "RemoteVisu auf " + IP + ":" + Port);

            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.METHOD_INVOKE, "ende");
        }

        /// <summary>
        /// Führt den eigentlichen Lernprozess aus
        /// </summary>
        public void _ExcecLearning()
        {
            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.METHOD_INVOKE, "start");
            
            _cls_Config _Config = _cls_Config._cfg;
            string _Mail_TEXT = "Lernen beendet!\r\n\r\nWeitere Meldungen:\r\n";

            try
            {
                cls_Rechen_LOG.RESET();

                //Visualiserung...
                //versuchen zu initialisieren
                INIT_Remote_Visual(_Config.Visual_IP, _Config.Visual_Port);
                //wenn visu verfügbar, je nach Config-Mode, diese Resetten
                if (this.cli_RM_Neuro_Visual != null)
                {
                    switch(_Config.mode)
                    {
                        case "Approximation":
                            this.cli_RM_frm_visual.RESET(_Config.mode, _Config.Object_Cache.Ap_data);
                            break;
                        case "TimeSeries":
                            this.cli_RM_frm_visual.RESET(_Config.mode, _Config.Object_Cache.TS_data, _Config.Object_Cache.TS_windowSize, _Config.Object_Cache.TS_predictionSize);
                            break;
                        default:
                            this.cli_RM_frm_visual.RESET(_Config.mode);
                            break;
                    }

                    cli_RM_frm_visual.INIT(_Config._I.GetLength(0));
                }

                if (!_Config.RESTART)
                {
                    // Netzwerk erstellen...
                    IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Lege Netzwerk an");
                    // AF festlegen
                    IVisual.I_ActivationFunction _AF = null;
                    switch (_Config.AF)
                    {
                        case IVisual.I_AF.Aktivierungsfunktion.SIGMOID:
                            _AF = new SigmoidFunction(_Config.alpha);
                            break;

                        case IVisual.I_AF.Aktivierungsfunktion.HYPERBOLICTANGENS:
                            _AF = new HyperbolicTangensFunction(_Config.alpha);
                            break;

                        case IVisual.I_AF.Aktivierungsfunktion.BIPOLARSIGMOID:
                            _AF = new BipolarSigmoidFunction(_Config.alpha);
                            break;
                    }
                    this._network = new Network(_AF, _Config.Input_Count, _Config._size);

                    IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Randomisieren");
                    this._network.Randomize();

                    // Teacher anlegen
                    IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Lege Lernalgorithmus fest");
                    IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Parameter: LR:" + _Config.learningRate + " MOM:" + _Config.momentum + " LL:" + _Config.learningLimit);
                    _teacher = new BackPropagationLearning(_network);
                }
                else
                {
                    IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "benutze restart");
                }
                // Lernparameter S1 festlegen
                _teacher.LearningRate = _Config.learningRate;
                _teacher.Momentum = _Config.momentum;
                _teacher.LearningLimit = _Config.learningLimit;
                bool is_S2 = false;

                double error = double.MaxValue;
                double last_err = double.MaxValue;
                int cycles = 0;

                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Ändere Runflag auf Running");
                this._LEARNSTATE = RUNFLAG.RUNNING;

                int ct = 0;

                // Für remote-debugging
                if (System.Diagnostics.Debugger.IsAttached)
                    System.Diagnostics.Debugger.Break();

                _Mail_TEXT += "Start um " + DateTime.Now.ToString() + "\r\n";

                while (true) 
                {
                    // Bedingung für Zyklenabbruch
                    if(!(cycles < 500 || !_Config.zyklenabbruch))
                    {
                        IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.INFO, "Zyklenabbruch beendet das lernen!");
                        break;
                    }

                    // Abbruch erzwungen?
                    if (this._LEARNSTATE == RUNFLAG.FORCESTOP)
                        break;

                    // Lernlimit unterschritten
                    if (error < _teacher.LearningLimit)
                    {
                        IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.INFO, "LernLimit unterschritten! is_S2=" + is_S2.ToString());
                        // Ist S2 konfiguriert und war das grad nicht s2
                        if (_Config.S2 && !is_S2)
                        {
                            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.INFO, "Wende S2 an");
                            _Mail_TEXT += DateTime.Now.ToString() + " Wende S2 an\r\n";

                            _teacher.LearningRate = _Config.learningRate2;
                            _teacher.Momentum = _Config.momentum2;
                            _teacher.LearningLimit = _Config.learningLimit2;
                            is_S2 = true;
                        }
                        else
                        {
                            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.INFO, "Beende Lernen");
                            _Mail_TEXT += DateTime.Now.ToString() + " Lernen beendet\r\n";
                            break;
                        }
                    }

                    //Bewusst abgeschafft, um genaueres zeichen zu ermöglichen!
                    //error = teacher.LearnEpoch(_I, _O);
                    //Application.DoEvents();
                    cls_Rechen_LOG.WRITE_LOG_LINE("\r\n--- LearnEpoch ---");
                    int i, n = _Config._I.Length;
                    error = 0;

                    // für alle EM
                    for (i = 0; i < n; i++)
                    {
                        cls_Rechen_LOG.WRITE_LOG_LINE("--- Pattern " + i + " ---");
                        error += _teacher.Learn(_Config._I[i], _Config._O[i]);

                        // Wenn eine visu verfügbar
                        if (this.cli_RM_Neuro_Visual != null)
                        {
                            try
                            {
                                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Zeiche Netz, da eine Visualiserung verfügbar ist.");
                                this.cli_RM_frm_visual.draw((IVisual.I_Network)this._network);
                            }catch(Exception ex)
                            {
                                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.ERROR, ex.Message);
                                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Deaktiviere Visualisierung, da ein Fehler (1) aufgetreten ist.");
                                this.cli_RM_Neuro_Visual = null;
                            }
                        }
                    }

                    this.LAST_ERROR = (float)error;

                    // Wenn eine visu ferfügbar
                    if (this.cli_RM_Neuro_Visual != null)
                    {
                        try
                        {
                            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Update Fehler, da eine Visualierung verfügbar ist.");
                            this.cli_RM_frm_visual.update_error(error, 0);
                            this.cli_RM_frm_visual.push_Rechen_Log(MLP.NeuralNet.cls_Rechen_LOG.VALUE());
                        }
                        catch (Exception ex)
                        {
                            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.ERROR, ex.Message);
                            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Deaktiviere Visualisierung, da ein Fehler (2) aufgetreten ist.");
                            this.cli_RM_Neuro_Visual = null;
                        }
                    }
                    IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Aktueller Fehler: " + error);
                    
                    _teacher.IsConverged = (error < _Config.learningLimit);
                    IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "IsConverged=" + _teacher.IsConverged);
                    
                    // Zähler für den Zyklenabbruch
                    ct++;
                    cycles++;

                    if (Math.Round(error, 4) != last_err)
                    {
                        last_err = Math.Round(error, 4);
                        cycles = 0;
                    }
                }

                // Wenn eine Visu verfügbar
                if (this.cli_RM_Neuro_Visual != null)
                {
                    IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Setze Finales Netzwerk, da eine Visualisierung verfügbar ist.");
                    this.cli_RM_frm_visual.SET_FINAL_NETWORK(this._network, _Config._I, _Config._O);
                }

                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.INFO, "Lernen beendet");
            }catch(Exception ex)
            {
                System.Diagnostics.StackTrace st = new System.Diagnostics.StackTrace(ex, true);
                System.Diagnostics.StackFrame frame = st.GetFrame(0);
                int line = frame.GetFileLineNumber();
                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.ERROR, ex.Message + " Zeile: " + line);

                _Mail_TEXT += DateTime.Now.ToString() + " Fehler während des Lernvorgangs!\r\n";
            }

            if (_Config.MAIL_Enabled)
            {
                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "Sende Mail");
                try
                {
                    System.Net.Mail.MailMessage mail = new System.Net.Mail.MailMessage(_Config.MAIL_FROM, _Config.MAIL_TO);
                    System.Net.Mail.SmtpClient client = new System.Net.Mail.SmtpClient();
                    client.Port = _Config.MAIL_Port;
                    client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network;
                    client.UseDefaultCredentials = false;
                    client.Host = _Config.MAIL_Host;
                    mail.Subject = _Config.MAIL_Betreff;
                    mail.Body = _Mail_TEXT;
                    if(_Config.MAIL_Auth)
                        client.Credentials = new System.Net.NetworkCredential(_Config.MAIL_USN, _Config.MAIL_PW);
                    client.Send(mail);
                }
                catch(Exception ex)
                {
                    IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.ERROR, ex.Message);
                }
            }

            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.METHOD_INVOKE, "ende");
        }

        /// <summary>
        /// Berechnet die Netzausgabe
        /// </summary>
        /// <param name="inp">Eingabemuster</param>
        /// <returns>Netzausgabe</returns>
        public float[] COMPUTE(float[] inp)
        {
            return this._network.Compute(inp);
        }

        /// <summary>
        /// Gbt den Fehler einer Brechnung zurück, nicht das Ergebnis
        /// </summary>
        /// <param name="input">Netzeingabe</param>
        /// <param name="output">erwartete netzausgabe</param>
        /// <returns>Fehler</returns>
        public float COMPUTE_and_CALCERR(float[] input, float[] output)
        {
            return this._teacher.COMPUTE_and_CALCERR(input, output);
        }

        /// <summary>
        /// Gibt den Inhalt der 3D-Fehler LV zuück (Neuronen und Layer)
        /// </summary>
        /// <returns></returns>
        public List<string> GET_3D_Dim()
        {
            List<string> _tmp = new List<string>();
            for (int l_i = 0; l_i < this._network.LayersCount; l_i++)
            {
                for (int n_i = 0; n_i < _network[l_i].NeuronsCount; n_i++)
                {
                    if (_network[l_i][n_i].InputsCount == 2)
                        _tmp.Add(l_i + " - " + n_i);
                }
            }

            return _tmp;
        }

        /// <summary>
        /// Gibt alle Gewichte un deren Postion im Netz zurück
        /// </summary>
        /// <returns></returns>
        public List<Tuple<string, double>> GET_WEIGHTS()
        {
            List<Tuple<string, double>> _ret = new List<Tuple<string, double>>();

            for (int x = 0; x < this._network.LayersCount; x++)
            {
                for (int i = 0; i < this._network[x].NeuronsCount; i++)
                {
                    for (int n = 0; n < this._network[x][i].InputsCount; n++)
                    {
                        _ret.Add(new Tuple<string, double>(x + " - " + i + " - " + n, this._network[x][i][n]));
                    }
                }
            }

            return _ret;
        }

        /// <summary>
        /// Setzt die Gewichte neu. MANUELLE NUTZUNG
        /// </summary>
        /// <param name="_W"></param>
        public void SET_WEIGHTS(List<Tuple<string, double>> _W)
        {
            for (int i = 0; i < _W.Count; i++)
            {
                string[] _spl = _W[i].Item1.Replace(" ", "").Split('-');
                int layer = Convert.ToInt32(_spl[0]);
                int Neuron = Convert.ToInt32(_spl[1]);
                int Gewicht = Convert.ToInt32(_spl[2]);

                this._network[layer][Neuron][Gewicht] = (float)_W[i].Item2;
            }
        }

        /// <summary>
        /// Gibt die Größe der Eingabe oder Ausgabepattern zurück
        /// </summary>
        /// <param name="I_O"></param>
        /// <returns></returns>
        public int COUNT(char I_O)
        {
            if (I_O == 'O')
                return _cls_Config._cfg._O[0].Length;
            else if(I_O == 'I')
                return _cls_Config._cfg._I[0].Length;
            else
                return -1;
        }

        /// <summary>
        /// Speichert ein Objekt in eine BINÄR-Datei
        /// </summary>
        /// <param name="PATH">Ziel-Pfad</param>
        /// <param name="o">Object</param>
        private void SERI(string PATH, object o)
        {
            System.IO.Stream stream = System.IO.File.Open(PATH, System.IO.FileMode.Create);
            System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            bFormatter.Serialize(stream, o);
            stream.Close();
        }

        /// <summary>
        /// Lädt ein Objekt aus einer BINÄR-Datei
        /// </summary>
        /// <param name="PATH">Quell-Pfad</param>
        /// <returns>Objekt</returns>
        private object DSERI(string PATH)
        {
            System.IO.Stream stream = System.IO.File.Open(PATH, System.IO.FileMode.Open);
            System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            object o = bFormatter.Deserialize(stream);
            stream.Close();

            return o;
        }

        /// <summary>
        /// Speichert das Netz in Dateien
        /// </summary>
        /// <param name="PATH">Speicherpfad</param>
        /// <param name="CFG">Mit Config?</param>
        /// <returns>OK?</returns>
        public bool SAVE_NET(string PATH, bool CFG)
        {
            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.METHOD_INVOKE, "start");
            try
            {
                SERI(PATH + "_NET", this._network);
                SERI(PATH + "_TEA", this._teacher);
                if (CFG)
                    SERI(PATH + "_CFG", new _cls_Config(_cls_Config._cfg));

                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.METHOD_INVOKE, "ende");
                return true;
            }
            catch (Exception ex)
            {
                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.ERROR, ex.Message);
                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.METHOD_INVOKE, "ende");
                return false;
            }
        }

        /// <summary>
        /// Lädt das Netz aus Dateien
        /// </summary>
        /// <param name="PATH">Pfad der Dateien</param>
        /// <returns>OK?</returns>
        public bool LOAD_NET(string PATH)
        {
            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.METHOD_INVOKE, "start");
            IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.DEBUG, "PFAD = " + PATH);

            try
            {
                this._network = (Network)DSERI(PATH + "_NET");
                this._teacher = (BackPropagationLearning)DSERI(PATH + "_TEA");
                if (System.IO.File.Exists(PATH + "_CFG"))
                {
                    IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.INFO, "Lade Config");
                    _cls_Config _cfg = new _cls_Config((_cls_Config)DSERI(PATH + "_CFG"));
                    _cfg.SET_CFG();
                }

                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.METHOD_INVOKE, "ende");
                return true;
            }
            catch(Exception ex)
            {
                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.ERROR, ex.Message);
                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.METHOD_INVOKE, "ende");

                return false;
            }
        }

        /// <summary>
        /// Berechnet die Daten für den 3D-Fehler
        /// </summary>
        /// <param name="l_i">Layer</param>
        /// <param name="n_i">Neuron</param>
        /// <param name="g_1">Gewicht 1</param>
        /// <param name="g_2">Gewicht 2</param>
        /// <param name="err_dist">Werte bereich um den mittelwert der Gewichte(+-)</param>
        /// <returns>Chart 3D-Data</returns>
        public Tuple<float[,], double, double, double> Calc_3D(int l_i, int n_i, int g_1, int g_2, double err_dist)
        {
            float[,] data = null;
            int range_min = 0;
            int range_max = 0;
            double delta = 0;

            try
            {
                float _0_v = this._network[l_i][n_i][0];
                float _1_v = this._network[l_i][n_i][1];

                delta = (Math.Abs(this._network[l_i][0][g_1]) + Math.Abs(this._network[l_i][0][g_2])) / 2;
                range_min = (int)(delta - err_dist);
                range_max = (int)(delta + err_dist);

                if (range_max < range_min)
                {
                    int tmp = range_min;
                    range_min = range_max;
                    range_max = tmp;
                }
                int anz = range_max - range_min;

                float d = (float)delta;// anz / (anz - 1);
                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.INFO, "3D-Delta: " + d);
                data = (float[,])Array.CreateInstance(typeof(float), anz, anz);
                for (int i = 0; i < anz; i = i + 1)
                {
                    for (int j = 0; j < anz; j = j + 1)
                    {
                        data[i, j] = 0;
                        this._network[l_i][n_i][0] = range_min + i * d;
                        this._network[l_i][n_i][1] = range_min + j * d;

                        for (int n = 0; n < _cls_Config._cfg._I.Length; n++)
                            data[i, j] += (float)Math.Pow(this._teacher.COMPUTE_and_CALCERR(_cls_Config._cfg._I[n], _cls_Config._cfg._O[n]), 2);
                    }
                }

                this._network[l_i][n_i][g_1] = _0_v;
                this._network[l_i][n_i][g_2] = _1_v;
            }
            catch (Exception ex)
            {
                IVisual._SC.cls_LOG.WRITE_LOG_LINE(IVisual._SC.cls_LOG.LOGLEVEL.ERROR, ex.Message);
            }

            return new Tuple<float[,], double, double, double>(data, range_min, range_max, delta);
        }
    }
}
